/*
    Copyright (c) 2009 Pavol Malosek a.k.a. malo

    This file is part of ComComLib.

    ComComLib is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 3 of the License, or
    (at your option) any later version.

    ComComLib is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include <iostream>
#include <assert.h>
#include <time.h>

#include <lcc/dlparser.hpp>

//#define LCC_PARSER_DEBUG
//#define LCC_PARSER_DEBUG_LEVEL 4

// level 1 = key behaviour
// level 2 = processing flow
// level 4 = infos

#ifndef LCC_PARSER_DEBUG
#   define lcc_log(n, ...)  while (0)
#else
#   define lcc_log(n, ...)    do { if ((n) <= LCC_PARSER_DEBUG_LEVEL) \
                                { printf (__VA_ARGS__);}} while (0)
#endif


lcc::parser_t::parser_t (lcc::device_info_t *device_info_, 
      uint16_t start_addr_) :
    device_info (NULL),
    state (state_unknown), 
    count_0xff (0), 
    header_pos (NULL),
    cur_per (0),
    cur_act_channels_count (0),
    cur_stamp (0),
    adc_value (0),
    adc_mem_pos (0),
    mem_pos (start_addr_),
    after_gap (false)
{

    //  Clean record_header buffer.
    memset (&record_header, '\0', sizeof (record_header_t));

    //  Set header pos to header begin.
    header_pos = (uint8_t*) &record_header;

    //  Clean cur_record, set all channles off.
    memset (&cur_record, '\0', sizeof (record_t));
    cur_record.values [0].input_type = in_off;
    cur_record.values [1].input_type = in_off;
    cur_record.values [2].input_type = in_off;
    cur_record.values [3].input_type = in_off;

    //  Store device infoo pointer.
    device_info = device_info_;
}

int lcc::parser_t::parse (const unsigned char *buff_, size_t size_)
{

    const unsigned char *pos = buff_;

    while (pos <= buff_ + size_) {
        
        lcc_log (4, "mem_pos 0x%x: 0x%x\n", mem_pos, *pos);
        //  In lincykl mode data between precord_prt and next record header
        //  are invalid and has to be ignored.
        if (device_info->lincyk && !after_gap && 
              mem_pos >= /*device_info->record_ptr*/ 0x1230) {
            state = state_data_ignore;
            after_gap = true;
        }
 
        //  0xff for lo byte is valid.
        if (*pos == 0xff && state != state_adc_byte_lo) {

            //  Record starts with 0xfe preceeded with 6 x 0xff.
            //  So we will count subsequent 0xff.
            count_0xff++;

            //  We do not know where we are.
            state = state_unknown;

            //  Reset cur*.
            cur_stamp = 0;
            cur_per = 0;
            cur_act_channels_count = 0;
        
        //  0xfe has special meaning only after 0xff
        } else if (*pos == 0xfe && state == state_unknown) {
           
            //  Check how many 0xff we have before.
            if (count_0xff >= 6) {
                lcc_log (4, "Record header found %i\n", (int)(pos - buff_));

                //  We have to be in unknown state;
                assert (state == state_unknown);

                //  We are now at record header.
                state = state_record_header;
            }

            //  Reset 0xff counter.
            count_0xff = 0;

        } else {
            
            //  Reset 0xff counter.
            count_0xff = 0;

            switch (state) {
                //  We believe that record header can not be interupted.
                case state_record_header:
                    //  Store value into header struct.
                    *header_pos = *pos;

                    //  Move further.
                    header_pos++;
                    
                    if (header_pos == (uint8_t*) &record_header + 
                          sizeof (record_header_t)) {
                        
                        //  We have parsed entire header record. Data folowws.
                        state = state_adc_byte_hi;

                        //  Reset header_pos.
                        header_pos = (uint8_t*) &record_header;

                        //  Recalculate BCD values in record header to int-s.
                        convert_record_header_values ();

                        lcc_log (4, "Record header parsed\n");
                    }
                    break;
                case state_adc_byte_hi:
                    //  Clean adc_value.
                    adc_value = 0;

                    //  HI byte from ADC value.
                    adc_value |= *pos << 8;

                    //  LO part of ADC value follows.
                    state = state_adc_byte_lo;
                    break;
                case state_adc_byte_lo:
                    //  LO byte.
                    adc_value |= *pos;

                    //  We have valid ADC value.
                    lcc_log (4, "Parsed value 0x%x\n", adc_value);
                        
                    //  Valid range for ADC is from 0 to 0xfdf9
                    if (adc_value > 0xfdf9) {
                        //  Interuptions
                        //  0xfdfa  stop by binary input
                        //  0xfdfb  stop by magnet
                        //  0xfdfc  low batery
                        //  0xfdfd  setting of RTC changed
                        //  0xfdfe  RESET command received
                        //  0xfdff  ADC range overflow
 
                        state = state_unknown;
                            
                        cur_stamp = 0;
                        cur_per = 0;
                        cur_act_channels_count = 0;

                        lcc_log (4, "Interuption\n");

                    } else {
                        //  Store input type and value into apropriate record.
                        unsigned int channel_idx = value_idx (adc_mem_pos);

                        cur_record.values [channel_idx].input_type = 
                            device_info->input_type [channel_idx];

                        cur_record.values [channel_idx].adc_value =
                            adc_value;
                            
                        //  Move to next adc_value.
                        adc_mem_pos++;

                        //  Next adc value HI byte follows.
                        state = state_adc_byte_hi;

                        //  Have entire record, store it to records list and 
                        //  move to the first adc_value.
                        if (adc_mem_pos % cur_act_channels_count == 0) {

                            lcc_log (4, "Have entire record, %s(%i)\n",
                                __FILE__, __LINE__);

                            //  Move to the first adc_value.
                            adc_mem_pos = 0;

                            //  Set stamp to the cur_record.
                            cur_record.stamp = cur_stamp;
                        
                            //  Store just produced record into the list.
                            records.push_back (cur_record);

                            //  Update timestamp for next reccord.
                            cur_stamp += cur_per;

                            //  Clean cur_record.
                            memset (&cur_record, '\0', sizeof (record_t));
                            cur_record.values [0].input_type = in_off;
                            cur_record.values [1].input_type = in_off;
                            cur_record.values [2].input_type = in_off;
                            cur_record.values [3].input_type = in_off;
                        }
                    }
                    break;
                case state_data_ignore:
                    //  Ignore data till next header
                    lcc_log (4, "Ignoring data state, %s(%i).\n",
                        __FILE__, __LINE__);
                    break;
                default:
                    lcc_log (4, "%s, %s(%i)", parser_state_to_string (state),
                        __FILE__, __LINE__);
                    assert (false);
            }
        }

        mem_pos++;    
        pos++;

    }

    return 0;
}

unsigned int lcc::parser_t::value_idx (unsigned int mem_order_)
{
    assert (device_info);

    //  Save orig channels mask.
    unsigned char chnls_mask = record_header.kanaly; 

    //  Active channles count.
    unsigned int valid_chnl = 0;
    
    //  Have to start count from mem_order_ + 1.
    mem_order_++;

    //  We can have max. 4 channles.
    for (unsigned int i = 0; i < max_channels; i++) {
        
        //  Active channel found. 
        if (chnls_mask & 0x01)
            valid_chnl++;

        //  We have found mem_order_ th active channel index.
        if (valid_chnl == mem_order_) {
            return i;
        }

        //  Move to next channel.
        chnls_mask = chnls_mask >> 1;
    }

    //  Should not be reached.
    assert (false);
    return 0;
}

unsigned int lcc::parser_t::flush_records (device_t *device_)
{
    size_t have_records = records.size ();

    if (have_records) {
        //  Flush records into device_t object.
        device_->add_records (records);
        records.clear ();
    }

    return have_records;
}

const char *lcc::parser_t::parser_state_to_string (parser_state_t state_)
{
    switch (state_) {
        case state_unknown:
            return "state_unknown";
        case state_record_header:
            return "state_record_header";
        case state_adc_byte_hi:
            return "state_adc_byte_hi";
        case state_adc_byte_lo:
            return "state_adc_byte_lo";
        default:
            return "unknown";
    }
}

void lcc::parser_t::convert_record_header_values (void)
{

#ifdef LCC_PARSER_DEBUG
    uint8_t *rh_ptr = (uint8_t*) &record_header;    

    while (rh_ptr != (uint8_t*) &record_header + sizeof (record_header_t)) {
        lcc_log (4, "[0x%x]", *rh_ptr);
        
        rh_ptr++;
    }
    lcc_log (4, "\n");
#endif
    
    record_header.seconds = bcd_to_uint8 (record_header.seconds);
    record_header.minutes = bcd_to_uint8 (record_header.minutes);
    record_header.hours = bcd_to_uint8 (record_header.hours);
    record_header.day = bcd_to_uint8 (record_header.day);
    record_header.months = bcd_to_uint8 (record_header.months);
    record_header.years = bcd_to_uint8 (record_header.years);

    //  Logging interval
    //
    //  per_h bit 0 - 10 sec
    //        bit 1 - 1 min
    //        bit 2 - 1 hod

    //  Set cur_per from current header.
    if (record_header.per_h & 0x1)
        cur_per = record_header.per_l * 10;
    else if (record_header.per_h & 0x2)
        cur_per = record_header.per_l * 60;
    else if (record_header.per_h & 0x4)
        cur_per = record_header.per_l * 60 * 60;
    else
        assert (false);

    //  Set cur_act_channels_count from current header.
    cur_act_channels_count = act_bits (record_header.kanaly);

    //  Have to find closest bigger integer multiple of per.
    //  For example if stamp is 21:54:10 and logging perios is 300s
    //  first recod will be logged at 21:55:00
    struct tm tm;
    memset (&tm, '\0', sizeof (tm));
    
    tm.tm_sec = record_header.seconds;
    tm.tm_min = record_header.minutes;
    tm.tm_hour = record_header.hours;
    tm.tm_mday = record_header.day;
    tm.tm_mon = record_header.months - 1;
    tm.tm_year = record_header.years + 100; 

    uint64_t stamp = mktime (&tm);
    
    //  Find time when first record was taken.
    cur_stamp = stamp - (stamp % cur_per) + cur_per;

    lcc_log (4, "record header: %02i:%02i:%02i, %02i.%02i.%02i, "\
        "per %is, chnls %i.\n",
        record_header.hours, record_header.minutes, record_header.seconds,
        record_header.day, record_header.months, record_header.years, 
        cur_per, cur_act_channels_count);
}
